<!DOCTYPE html>
<html lang='zh-CN'>

<head>
  <meta name="generator" content="Hexo 6.3.0">
  <meta name="hexo-theme" content="https://github.com/xaoxuu/hexo-theme-stellar/tree/1.19.0">
  <meta charset="utf-8">
  

  <meta http-equiv='x-dns-prefetch-control' content='on' />
  <link rel='dns-prefetch' href='https://gcore.jsdelivr.net'>
  <link rel="preconnect" href="https://gcore.jsdelivr.net" crossorigin>
  <link rel='dns-prefetch' href='//unpkg.com'>

  <meta name="renderer" content="webkit">
  <meta name="force-rendering" content="webkit">
  <meta http-equiv="X-UA-Compatible" content="IE=Edge,chrome=1">
  <meta name="HandheldFriendly" content="True" >
  <meta name="apple-mobile-web-app-capable" content="yes">
  <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
  <meta name="theme-color" content="#f8f8f8">
  
  <title>Redis实现排行榜 - 愔颂</title>

  
    <meta name="description" content="基于Redis的基本数据结构和业务逻辑设计实现用户活跃排行榜。">
<meta property="og:type" content="article">
<meta property="og:title" content="Redis实现排行榜">
<meta property="og:url" content="https://farhills.gitee.io/2023/10/05/Redis%E5%AE%9E%E7%8E%B0%E6%8E%92%E8%A1%8C%E6%A6%9C/index.html">
<meta property="og:site_name" content="愔颂">
<meta property="og:description" content="基于Redis的基本数据结构和业务逻辑设计实现用户活跃排行榜。">
<meta property="og:locale" content="zh_CN">
<meta property="og:image" content="https://cdn.nlark.com/yuque/0/2023/png/36098302/1695536770890-456ec61e-4cfd-4f41-aa60-f6b71a94f7cb.png">
<meta property="og:image" content="https://cdn.nlark.com/yuque/0/2023/png/36098302/1695544852919-d968b842-6aa6-4549-b430-df58dc8895fa.png">
<meta property="article:published_time" content="2023-10-05T03:13:35.020Z">
<meta property="article:modified_time" content="2023-10-04T10:54:20.236Z">
<meta property="article:author" content="远岫">
<meta name="twitter:card" content="summary">
<meta name="twitter:image" content="https://cdn.nlark.com/yuque/0/2023/png/36098302/1695536770890-456ec61e-4cfd-4f41-aa60-f6b71a94f7cb.png">
  
  
  
  

  <!-- feed -->
  

  
    
<link rel="stylesheet" href="/css/main.css">

  

  
    <link rel="shortcut icon" href="https://z1.ax1x.com/2023/10/05/pPXijyT.png">
  

  

  


  
</head>

<body>
  




  <div class='l_body' id='start'>
    <aside class='l_left' layout='post'>
    

  

<header class="header"><div class="logo-wrap"><a class="avatar" href="/about/"><div class="bg" style="opacity:0;background-image:url(https://gcore.jsdelivr.net/gh/cdn-x/placeholder@1.0.4/avatar/round/rainbow64@3x.webp);"></div><img no-lazy class="avatar" src="https://s1.ax1x.com/2022/11/12/ziJjfK.jpg" onerror="javascript:this.classList.add('error');this.src='https://gcore.jsdelivr.net/gh/cdn-x/placeholder@1.0.4/image/2659360.svg';"></a><a class="title" href="/"><div class="main" ff="title">愔颂</div></a></div>

<nav class="menu dis-select"><a class="nav-item active" href="/">文章</a><a class="nav-item" href="/friends/">收藏</a><a class="nav-item" href="/about/">关于</a></nav>
</header>


<div class="widgets">
<widget class="widget-wrapper search"><div class="widget-body"><div class="search-wrapper" id="search"><form class="search-form"><input type="text" class="search-input" id="search-input" data-filter="/blog/" placeholder="文章搜索"><svg t="1670596976048" class="icon search-icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2676" width="200" height="200"><path d="M938.2 832.6L723.8 618.1c-2.5-2.5-5.3-4.4-7.9-6.4 36.2-55.6 57.3-121.8 57.3-193.1C773.3 222.8 614.6 64 418.7 64S64 222.8 64 418.6c0 195.9 158.8 354.6 354.6 354.6 71.3 0 137.5-21.2 193.2-57.4 2 2.7 3.9 5.4 6.3 7.8L832.5 938c14.6 14.6 33.7 21.9 52.8 21.9 19.1 0 38.2-7.3 52.8-21.8 29.2-29.1 29.2-76.4 0.1-105.5M418.7 661.3C284.9 661.3 176 552.4 176 418.6 176 284.9 284.9 176 418.7 176c133.8 0 242.6 108.9 242.6 242.7 0 133.7-108.9 242.6-242.6 242.6" p-id="2677"></path></svg></form><div id="search-result"></div><div class="search-no-result">没有找到内容！</div></div></div></widget>


<widget class="widget-wrapper toc single" id="data-toc"><div class="widget-header cap dis-select"><span class="name">Redis实现排行榜</span></div><div class="widget-body fs14"><div class="doc-tree active"><ol class="toc"><li class="toc-item toc-level-2"><a class="toc-link" href="#1-1%E3%80%81%E5%9C%BA%E6%99%AF%E8%AF%B4%E6%98%8E"><span class="toc-text">1.1、场景说明</span></a></li><li class="toc-item toc-level-2"><a class="toc-link" href="#1-2%E3%80%81%E6%96%B9%E6%A1%88%E8%AE%BE%E8%AE%A1"><span class="toc-text">1.2、方案设计</span></a></li><li class="toc-item toc-level-2"><a class="toc-link" href="#1-3%E3%80%81%E6%8E%92%E8%A1%8C%E6%A6%9C%E5%AE%9E%E7%8E%B0"><span class="toc-text">1.3、排行榜实现</span></a><ol class="toc-child"><li class="toc-item toc-level-3"><a class="toc-link" href="#1-3-1%E3%80%81%E4%B8%9A%E5%8A%A1%E5%AE%9E%E4%BD%93%E8%AE%BE%E8%AE%A1"><span class="toc-text">1.3.1、业务实体设计</span></a></li><li class="toc-item toc-level-3"><a class="toc-link" href="#1-3-2%E3%80%81%E8%AE%A1%E7%AE%97%E6%B4%BB%E8%B7%83%E5%BA%A6"><span class="toc-text">1.3.2、计算活跃度</span></a></li><li class="toc-item toc-level-3"><a class="toc-link" href="#1-3-3%E3%80%81%E8%A7%A6%E5%8F%91%E6%B4%BB%E8%B7%83%E5%BA%A6%E6%9B%B4%E6%96%B0"><span class="toc-text">1.3.3、触发活跃度更新</span></a></li><li class="toc-item toc-level-3"><a class="toc-link" href="#1-3-4%E3%80%81%E6%8E%92%E8%A1%8C%E6%A6%9C%E6%9F%A5%E8%AF%A2"><span class="toc-text">1.3.4、排行榜查询</span></a></li></ol></li></ol></div></div></widget>




</div>


    </aside>
    <div class='l_main'>
      

      



<div class="bread-nav fs12"><div id="breadcrumb"><a class="cap breadcrumb" href="/">主页</a><span class="sep"></span><a class="cap breadcrumb" href="/">文章</a><span class="sep"></span><a class="cap breadcrumb-link" href="/categories/%E9%A1%B9%E7%9B%AE%E5%AE%9E%E6%88%98/">项目实战</a></div><div id="post-meta">发布于&nbsp;<time datetime="2023-10-05T03:13:35.020Z">2023-10-05</time></div></div>

<article class='md-text content post'>
<h1 class="article-title"><span>Redis实现排行榜</span></h1>
<meta name="referrer" content="no-referrer"/>

<p>基于Redis的基本数据结构和业务逻辑设计实现用户活跃排行榜。</p>
<span id="more"></span>

<h2 id="1-1、场景说明"><a href="#1-1、场景说明" class="headerlink" title="1.1、场景说明"></a>1.1、场景说明</h2><p>技术派中，设计了一个社区用户的活跃排行榜，包括日榜和月榜。</p>
<p><strong>用户活跃度计算方式：</strong></p>
<ol>
<li>用户每访问一个新的页面， +1分</li>
<li>对于一篇文章，点赞、收藏， +2分；取消点赞、取消收藏，将之前的活跃分收回。</li>
<li>文章评论， +3分</li>
<li>发布一篇审核通过的文章， +10分</li>
</ol>
<p><strong>榜单：</strong><br>展示活跃度最高的前三十名用户。</p>
<p>效果如下：</p>
<div class="tag-plugin image"><div class="image-bg"><img src="https://cdn.nlark.com/yuque/0/2023/png/36098302/1695536770890-456ec61e-4cfd-4f41-aa60-f6b71a94f7cb.png" fancybox="true"/></div></div>



<h2 id="1-2、方案设计"><a href="#1-2、方案设计" class="headerlink" title="1.2、方案设计"></a>1.2、方案设计</h2><p>使用Redis的ZSet数据结构实现，以下是ZSet的简介：</p>
<p>Redis的ZSet（有序集合）是一种有序的、唯一的数据结构。它类似于Set，但每个元素都关联着一个分数（score），用于进行排序。</p>
<p>ZSet的特点包括：</p>
<ol>
<li>有序性：ZSet中的元素按照分数进行排序，可以根据分数进行范围查询、区间获取等操作。 </li>
<li>唯一性：ZSet中的元素是唯一的，不会存在重复的元素。 </li>
<li>快速的插入和删除：ZSet使用了跳跃表（Skip List）和哈希表（Hash Table）的结合体，使得插入和删除操作的时间复杂度为O(log N)。 </li>
<li>高效的查找：通过索引和跳跃表的特性，可以在O(log N)的时间复杂度内查找某个元素。</li>
</ol>
<p>ZSet常用的操作包括：</p>
<ul>
<li>ZADD：向ZSet中添加一个元素，同时指定其分数。</li>
<li>ZREM：从ZSet中移除一个元素。</li>
<li>ZRANGE：按照分数的顺序，获取指定范围内的元素。</li>
<li>ZSCORE：获取指定元素的分数。</li>
<li>ZINCRBY：将指定元素的分数增加一个特定的值。</li>
</ul>
<p>ZSet广泛应用于排行榜、计分系统、排行榜、时间轴等场景，提供了高效的排序和检索功能。</p>
<h2 id="1-3、排行榜实现"><a href="#1-3、排行榜实现" class="headerlink" title="1.3、排行榜实现"></a>1.3、排行榜实现</h2><h3 id="1-3-1、业务实体设计"><a href="#1-3-1、业务实体设计" class="headerlink" title="1.3.1、业务实体设计"></a>1.3.1、业务实体设计</h3><p>​       我们先实现一个更新用户活跃的方法，首先定义一个涵盖该业务场景的参数传递实体 ActivityScoreBo，记录用户活动（是否访问页面、点赞、收藏、评论、关注、发布文章）。</p>
<figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Data</span></span><br><span class="line"><span class="meta">@Accessors(chain = true)</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">ActivityScoreBo</span> &#123;</span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 访问页面增加活跃度</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="keyword">private</span> String path;</span><br><span class="line"></span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 目标文章</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="keyword">private</span> Long articleId;</span><br><span class="line"></span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 评论增加活跃度</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="keyword">private</span> Boolean rate;</span><br><span class="line"></span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 点赞增加活跃度</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="keyword">private</span> Boolean praise;</span><br><span class="line"></span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 收藏增加活跃度</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="keyword">private</span> Boolean collect;</span><br><span class="line"></span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 发布文章增加活跃度</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="keyword">private</span> Boolean publishArticle;</span><br><span class="line"></span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 被关注的用户</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="keyword">private</span> Long followedUserId;</span><br><span class="line"></span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 关注增加活跃度</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="keyword">private</span> Boolean follow;</span><br><span class="line">&#125;  </span><br></pre></td></tr></table></figure>



<p>有了业务实体，进一步我们需要计算活跃度。活跃度包括日榜和月榜，如下为对应的key生成。</p>
<figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 当天活跃度排行榜</span></span><br><span class="line"><span class="comment">     *</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@return</span> 当天排行榜key</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="keyword">private</span> String <span class="title function_">todayRankKey</span><span class="params">()</span> &#123;</span><br><span class="line">        <span class="keyword">return</span> ACTIVITY_SCORE_KEY + DateUtil.format(DateTimeFormatter.ofPattern(<span class="string">&quot;yyyyMMdd&quot;</span>), System.currentTimeMillis());</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 本月排行榜</span></span><br><span class="line"><span class="comment">     *</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@return</span> 月度排行榜key</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="keyword">private</span> String <span class="title function_">monthRankKey</span><span class="params">()</span> &#123;</span><br><span class="line">        <span class="keyword">return</span> ACTIVITY_SCORE_KEY + DateUtil.format(DateTimeFormatter.ofPattern(<span class="string">&quot;yyyyMM&quot;</span>), System.currentTimeMillis());</span><br><span class="line">    &#125;</span><br></pre></td></tr></table></figure>



<h3 id="1-3-2、计算活跃度"><a href="#1-3-2、计算活跃度" class="headerlink" title="1.3.2、计算活跃度"></a>1.3.2、计算活跃度</h3><p>（1）根据传入的业务实体，判断文章浏览、点赞、收藏、关注、排列、发文等行为，并计算对应的字段field和分数score，不同行为的加分机制见上述场景说明。</p>
<p>（2）接着，根据用户ID和当天日期（日为单位）生成一个唯一的活跃度信息的键值（userActionKey）。</p>
<p>（3）通过RedisClient从Redis中获取该userActionKey键对应的field值（ans）。</p>
<p>（4）如果ans为null，说明之前没有加分记录，执行加分操作： </p>
<ul>
<li><ul>
<li>如果加分数大于0，将加分记录保存到Redis的hash结构中，并设置有效期为一个月。&#x2F;注：【加分记录使用Redis的Hash结构存储，key为userActionKey，字段为field，value为分数score】</li>
<li>更新当天和当月的活跃度排行榜，使用Redis的zIncrBy函数。</li>
<li>如果新的活跃度得分大于等于加分数，更新日活跃榜单和月活跃榜单的有效期。</li>
</ul>
</li>
</ul>
<p>（5）如果ans大于0，说明之前该field已经加过分，继续判断： </p>
<ul>
<li><ul>
<li>如果分数小于0，说明是减分行为，应从Redis中删除加分记录。</li>
<li>更新当天和当月的活跃度排行榜。</li>
</ul>
</li>
</ul>
<p>幂等策略：</p>
<p>在上述加分操作中，为了防止重复加活跃度，我们做了一个幂等操作。</p>
<p>就是将用户的每个加分项，都记录下来，在执行具体加分时，基于此来做幂等判定 。</p>
<p>因此，我们对每个用户维护一个活跃更新的操作历史记录表，保存在redis的hash数据结构中，每天一个记录。</p>
<figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">key: activity_rank_&#123;user_id&#125;_&#123;年月日&#125; </span><br><span class="line">field: 活跃度更新key</span><br><span class="line">value: 添加的活跃度  </span><br></pre></td></tr></table></figure>



<p>思考：</p>
<p>1、事务问题：虽然单个redis操作是原子性的，但多次的redis操作，存在事务问题。</p>
<p>2、并发问题：没有做并发，幂等无法100%生效，依然可能存在重复添加&#x2F;扣减活跃度的情况  </p>
<p>问题一：</p>
<p>通过最终一致性（Eventual Consistency）来解决多次Redis操作的事务问题是一种常见的方法。最终一致性是指在分布式系统中，经过一段时间后，系统的所有副本最终会达到一致的状态。</p>
<p>在Redis中，可以使用以下方法来实现最终一致性：</p>
<ol>
<li>批量操作：将多个操作组合成一个批量操作，减少网络往返的次数。例如，使用管道（Pipeline）来发送多个命令，然后一次性获取它们的响应。 </li>
<li>异步操作：将操作异步化，即将操作放入消息队列或任务队列中，由后台线程或其他服务异步处理。这样可以避免阻塞主线程，并允许操作在不同的时间点执行。 </li>
<li>回滚机制：在执行操作之前，先将相关数据备份或记录下来。如果操作失败，可以使用备份数据进行回滚操作。 </li>
<li>重试机制：如果某个操作失败，可以进行重试，直到操作成功或达到最大重试次数。 </li>
<li>业务层面的补偿机制：如果操作失败，可以通过业务逻辑来进行补偿操作，以达到一致性。</li>
</ol>
<p>需要注意的是，最终一致性并不能提供强一致性的保证，因此在某些场景下可能会出现数据不一致的情况。在选择使用最终一致性来解决事务问题时，需要根据具体的业务需求和数据一致性要求来评估和权衡。</p>
<p>问题二：</p>
<p>通过加锁解决并发问题。</p>
<figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">addActivityScore</span><span class="params">(Long userId, ActivityScoreBo activityScore)</span> &#123;</span><br><span class="line">        <span class="keyword">if</span> (userId == <span class="literal">null</span>) &#123;</span><br><span class="line">            <span class="keyword">return</span>;</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        <span class="comment">// 1. 计算活跃度(正为加活跃,负为减活跃)</span></span><br><span class="line">        String field;</span><br><span class="line">        <span class="type">int</span> <span class="variable">score</span> <span class="operator">=</span> <span class="number">0</span>;</span><br><span class="line">        <span class="keyword">if</span> (activityScore.getPath() != <span class="literal">null</span>) &#123;</span><br><span class="line">            field = <span class="string">&quot;path_&quot;</span> + activityScore.getPath();</span><br><span class="line">            score = <span class="number">1</span>;</span><br><span class="line">        &#125; <span class="keyword">else</span> <span class="keyword">if</span> (activityScore.getArticleId() != <span class="literal">null</span>) &#123;</span><br><span class="line">            field = activityScore.getArticleId() + <span class="string">&quot;_&quot;</span>;</span><br><span class="line">            <span class="keyword">if</span> (activityScore.getPraise() != <span class="literal">null</span>) &#123;</span><br><span class="line">                field += <span class="string">&quot;praise&quot;</span>;</span><br><span class="line">                score = BooleanUtils.isTrue(activityScore.getPraise()) ? <span class="number">2</span> : -<span class="number">2</span>;</span><br><span class="line">            &#125; <span class="keyword">else</span> <span class="keyword">if</span> (activityScore.getCollect() != <span class="literal">null</span>) &#123;</span><br><span class="line">                field += <span class="string">&quot;collect&quot;</span>;</span><br><span class="line">                score = BooleanUtils.isTrue(activityScore.getCollect()) ? <span class="number">2</span> : -<span class="number">2</span>;</span><br><span class="line">            &#125; <span class="keyword">else</span> <span class="keyword">if</span> (activityScore.getRate() != <span class="literal">null</span>) &#123;</span><br><span class="line">                <span class="comment">// 评论回复</span></span><br><span class="line">                field += <span class="string">&quot;rate&quot;</span>;</span><br><span class="line">                score = BooleanUtils.isTrue(activityScore.getRate()) ? <span class="number">3</span> : -<span class="number">3</span>;</span><br><span class="line">            &#125; <span class="keyword">else</span> <span class="keyword">if</span> (BooleanUtils.isTrue(activityScore.getPublishArticle())) &#123;</span><br><span class="line">                <span class="comment">// 发布文章</span></span><br><span class="line">                field += <span class="string">&quot;publish&quot;</span>;</span><br><span class="line">                score += <span class="number">10</span>;</span><br><span class="line">            &#125;</span><br><span class="line">        &#125; <span class="keyword">else</span> <span class="keyword">if</span> (activityScore.getFollowedUserId() != <span class="literal">null</span>) &#123;</span><br><span class="line">            field = activityScore.getFollowedUserId() + <span class="string">&quot;_follow&quot;</span>;</span><br><span class="line">            score = BooleanUtils.isTrue(activityScore.getFollow()) ? <span class="number">2</span> : -<span class="number">2</span>;</span><br><span class="line">        &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">            <span class="keyword">return</span>;</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        <span class="keyword">final</span> <span class="type">String</span> <span class="variable">todayRankKey</span> <span class="operator">=</span> todayRankKey();</span><br><span class="line">        <span class="keyword">final</span> <span class="type">String</span> <span class="variable">monthRankKey</span> <span class="operator">=</span> monthRankKey();</span><br><span class="line">        <span class="comment">// 2. 幂等：判断之前是否有更新过相关的活跃度信息</span></span><br><span class="line">        <span class="keyword">final</span> <span class="type">String</span> <span class="variable">userActionKey</span> <span class="operator">=</span> ACTIVITY_SCORE_KEY + userId + DateUtil.format(DateTimeFormatter.ofPattern(<span class="string">&quot;yyyyMMdd&quot;</span>), System.currentTimeMillis());</span><br><span class="line">        <span class="type">Integer</span> <span class="variable">ans</span> <span class="operator">=</span> RedisClient.hGet(userActionKey, field, Integer.class);</span><br><span class="line">        <span class="keyword">if</span> (ans == <span class="literal">null</span>) &#123;</span><br><span class="line">            <span class="comment">// 2.1 之前没有加分记录，执行具体的加分</span></span><br><span class="line">            <span class="keyword">if</span> (score &gt; <span class="number">0</span>) &#123;</span><br><span class="line">                <span class="comment">// 记录加分记录</span></span><br><span class="line">                RedisClient.hSet(userActionKey, field, score);</span><br><span class="line">                <span class="comment">// 个人用户的操作记录，保存一个月的有效期，方便用户查询自己最近31天的活跃情况</span></span><br><span class="line">                RedisClient.expire(userActionKey, <span class="number">31</span> * DateUtil.ONE_DAY_SECONDS);</span><br><span class="line"></span><br><span class="line">                <span class="comment">// 更新当天和当月的活跃度排行榜</span></span><br><span class="line">                <span class="type">Double</span> <span class="variable">newAns</span> <span class="operator">=</span> RedisClient.zIncrBy(todayRankKey, String.valueOf(userId), score);</span><br><span class="line">                RedisClient.zIncrBy(monthRankKey, String.valueOf(userId), score);</span><br><span class="line">                <span class="keyword">if</span> (log.isDebugEnabled()) &#123;</span><br><span class="line">                    log.info(<span class="string">&quot;活跃度更新加分! key#field = &#123;&#125;#&#123;&#125;, add = &#123;&#125;, newScore = &#123;&#125;&quot;</span>, todayRankKey, userId, score, newAns);</span><br><span class="line">                &#125;</span><br><span class="line">                <span class="keyword">if</span> (newAns &lt;= score) &#123;</span><br><span class="line">                    <span class="comment">// 日活跃榜单，保存31天；月活跃榜单，保存1年</span></span><br><span class="line">                    RedisClient.expire(todayRankKey, <span class="number">31</span> * DateUtil.ONE_DAY_SECONDS);</span><br><span class="line">                    RedisClient.expire(monthRankKey, <span class="number">12</span> * DateUtil.ONE_MONTH_SECONDS);</span><br><span class="line">                &#125;</span><br><span class="line">            &#125;</span><br><span class="line">        &#125; <span class="keyword">else</span> <span class="keyword">if</span> (ans &gt; <span class="number">0</span>) &#123;</span><br><span class="line">            <span class="comment">// 2.2 之前已经加过分，因此这次减分可以执行</span></span><br><span class="line">            <span class="keyword">if</span> (score &lt; <span class="number">0</span>) &#123;</span><br><span class="line">                <span class="type">Boolean</span> <span class="variable">oldHave</span> <span class="operator">=</span> RedisClient.hDel(userActionKey, field);</span><br><span class="line">                <span class="keyword">if</span> (BooleanUtils.isTrue(oldHave)) &#123;</span><br><span class="line">                    <span class="type">Double</span> <span class="variable">newAns</span> <span class="operator">=</span> RedisClient.zIncrBy(todayRankKey, String.valueOf(userId), score);</span><br><span class="line">                    RedisClient.zIncrBy(monthRankKey, String.valueOf(userId), score);</span><br><span class="line">                    <span class="keyword">if</span> (log.isDebugEnabled()) &#123;</span><br><span class="line">                        log.info(<span class="string">&quot;活跃度更新减分! key#field = &#123;&#125;#&#123;&#125;, add = &#123;&#125;, newScore = &#123;&#125;&quot;</span>, todayRankKey, userId, score, newAns);</span><br><span class="line">                    &#125;</span><br><span class="line">                &#125;</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br></pre></td></tr></table></figure>



<h3 id="1-3-3、触发活跃度更新"><a href="#1-3-3、触发活跃度更新" class="headerlink" title="1.3.3、触发活跃度更新"></a>1.3.3、触发活跃度更新</h3><p>前面只是提供了一个增加活跃度的方法，但是什么时候调用它呢？</p>
<p>我们这里借助之前实现 Event&#x2F;Listenter方式来处理活跃度更新。</p>
<p>文章&#x2F;用户的相关操作事件监听，并更新对应的活跃度：</p>
<figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">UserActivityListener</span> &#123;</span><br><span class="line">    <span class="meta">@Autowired</span></span><br><span class="line">    <span class="keyword">private</span> UserActivityRankService userActivityRankService;</span><br><span class="line"></span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 用户操作行为，增加对应的积分</span></span><br><span class="line"><span class="comment">     *</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@param</span> msgEvent</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="meta">@EventListener(classes = NotifyMsgEvent.class)</span></span><br><span class="line">    <span class="meta">@Async</span></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">notifyMsgListener</span><span class="params">(NotifyMsgEvent msgEvent)</span> &#123;</span><br><span class="line">        <span class="keyword">switch</span> (msgEvent.getNotifyType()) &#123;</span><br><span class="line">            <span class="keyword">case</span> COMMENT:</span><br><span class="line">            <span class="keyword">case</span> REPLY:</span><br><span class="line">                <span class="type">CommentDO</span> <span class="variable">comment</span> <span class="operator">=</span> (CommentDO) msgEvent.getContent();</span><br><span class="line">                userActivityRankService.addActivityScore(ReqInfoContext.getReqInfo().getUserId(), <span class="keyword">new</span> <span class="title class_">ActivityScoreBo</span>()</span><br><span class="line">                        .setRate(<span class="literal">true</span>).setArticleId(comment.getArticleId()));</span><br><span class="line">                <span class="keyword">break</span>;</span><br><span class="line">            <span class="keyword">case</span> COLLECT:</span><br><span class="line">                <span class="type">UserFootDO</span> <span class="variable">foot</span> <span class="operator">=</span> (UserFootDO) msgEvent.getContent();</span><br><span class="line">                userActivityRankService.addActivityScore(ReqInfoContext.getReqInfo().getUserId(), <span class="keyword">new</span> <span class="title class_">ActivityScoreBo</span>().setCollect(<span class="literal">true</span>).setArticleId(foot.getDocumentId()));</span><br><span class="line">                <span class="keyword">break</span>;</span><br><span class="line">            <span class="keyword">case</span> CANCEL_COLLECT:</span><br><span class="line">                foot = (UserFootDO) msgEvent.getContent();</span><br><span class="line">                userActivityRankService.addActivityScore(ReqInfoContext.getReqInfo().getUserId(), <span class="keyword">new</span> <span class="title class_">ActivityScoreBo</span>().setCollect(<span class="literal">false</span>).setArticleId(foot.getDocumentId()));</span><br><span class="line">                <span class="keyword">break</span>;</span><br><span class="line">            <span class="keyword">case</span> PRAISE:</span><br><span class="line">                foot = (UserFootDO) msgEvent.getContent();</span><br><span class="line">                userActivityRankService.addActivityScore(ReqInfoContext.getReqInfo().getUserId(), <span class="keyword">new</span> <span class="title class_">ActivityScoreBo</span>().setPraise(<span class="literal">true</span>).setArticleId(foot.getDocumentId()));</span><br><span class="line">                <span class="keyword">break</span>;</span><br><span class="line">            <span class="keyword">case</span> CANCEL_PRAISE:</span><br><span class="line">                foot = (UserFootDO) msgEvent.getContent();</span><br><span class="line">                userActivityRankService.addActivityScore(ReqInfoContext.getReqInfo().getUserId(), <span class="keyword">new</span> <span class="title class_">ActivityScoreBo</span>().setPraise(<span class="literal">false</span>).setArticleId(foot.getDocumentId()));</span><br><span class="line">                <span class="keyword">break</span>;</span><br><span class="line">            <span class="keyword">case</span> FOLLOW:</span><br><span class="line">                <span class="type">UserRelationDO</span> <span class="variable">relation</span> <span class="operator">=</span> (UserRelationDO) msgEvent.getContent();</span><br><span class="line">                userActivityRankService.addActivityScore(ReqInfoContext.getReqInfo().getUserId(), <span class="keyword">new</span> <span class="title class_">ActivityScoreBo</span>().setFollow(<span class="literal">true</span>).setArticleId(relation.getUserId()));</span><br><span class="line">                <span class="keyword">break</span>;</span><br><span class="line">            <span class="keyword">case</span> CANCEL_FOLLOW:</span><br><span class="line">                relation = (UserRelationDO) msgEvent.getContent();</span><br><span class="line">                userActivityRankService.addActivityScore(ReqInfoContext.getReqInfo().getUserId(), <span class="keyword">new</span> <span class="title class_">ActivityScoreBo</span>().setFollow(<span class="literal">false</span>).setArticleId(relation.getUserId()));</span><br><span class="line">                <span class="keyword">break</span>;</span><br><span class="line">            <span class="keyword">default</span>:</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 发布文章，更新对应的积分</span></span><br><span class="line"><span class="comment">     *</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@param</span> event</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="meta">@Async</span></span><br><span class="line">    <span class="meta">@EventListener(ArticleMsgEvent.class)</span></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">publishArticleListener</span><span class="params">(ArticleMsgEvent&lt;ArticleDO&gt; event)</span> &#123;</span><br><span class="line">        <span class="type">ArticleEventEnum</span> <span class="variable">type</span> <span class="operator">=</span> event.getType();</span><br><span class="line">        <span class="keyword">if</span> (type == ArticleEventEnum.ONLINE) &#123;</span><br><span class="line">            userActivityRankService.addActivityScore(ReqInfoContext.getReqInfo().getUserId(), <span class="keyword">new</span> <span class="title class_">ActivityScoreBo</span>().setPublishArticle(<span class="literal">true</span>).setArticleId(event.getContent().getId()));</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>



<p>接下来，就是对应方法触发后事件更新了，包括ArticleMsgEvent、NotifyMsgEvent等。</p>
<div class="tag-plugin image"><div class="image-bg"><img src="https://cdn.nlark.com/yuque/0/2023/png/36098302/1695544852919-d968b842-6aa6-4549-b430-df58dc8895fa.png" fancybox="true"/></div></div>

<p>另外，针对用户浏览页面的活跃度触发，我们在 Filte&#x2F;Inteceptor 层实现，通过GlobalViewInterceptor的preHandle方法实现。</p>
<figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="type">boolean</span> <span class="title function_">preHandle</span><span class="params">(HttpServletRequest request, HttpServletResponse response, Object handler)</span> <span class="keyword">throws</span> Exception &#123;</span><br><span class="line">        <span class="keyword">if</span> (handler <span class="keyword">instanceof</span> HandlerMethod) &#123;</span><br><span class="line">            <span class="type">HandlerMethod</span> <span class="variable">handlerMethod</span> <span class="operator">=</span> (HandlerMethod) handler;</span><br><span class="line">            <span class="type">Permission</span> <span class="variable">permission</span> <span class="operator">=</span> handlerMethod.getMethod().getAnnotation(Permission.class);</span><br><span class="line">            <span class="keyword">if</span> (permission == <span class="literal">null</span>) &#123;</span><br><span class="line">                permission = handlerMethod.getBeanType().getAnnotation(Permission.class);</span><br><span class="line">            &#125;</span><br><span class="line"></span><br><span class="line">            <span class="keyword">if</span> (permission == <span class="literal">null</span> || permission.role() == UserRole.ALL) &#123;</span><br><span class="line">                <span class="keyword">if</span> (ReqInfoContext.getReqInfo() != <span class="literal">null</span>) &#123;</span><br><span class="line">                    <span class="comment">// 用户活跃度更新</span></span><br><span class="line">                    SpringUtil.getBean(UserActivityRankService.class).addActivityScore(ReqInfoContext.getReqInfo().getUserId(), <span class="keyword">new</span> <span class="title class_">ActivityScoreBo</span>().setPath(ReqInfoContext.getReqInfo().getPath()));</span><br><span class="line">                &#125;</span><br><span class="line">                <span class="keyword">return</span> <span class="literal">true</span>;</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="keyword">return</span> <span class="literal">true</span>;</span><br><span class="line">    &#125;</span><br></pre></td></tr></table></figure>



<h3 id="1-3-4、排行榜查询"><a href="#1-3-4、排行榜查询" class="headerlink" title="1.3.4、排行榜查询"></a>1.3.4、排行榜查询</h3><p>接下来就是将这个榜单展示给用户看。</p>
<p>基本流程如下： </p>
<p>1、从redis中获取topN的用户+评分 </p>
<p>2、查询用户的信息 </p>
<p>3、根据用户评分进行排序，并更新每个用户的排名</p>
<figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> List&lt;RankItemDTO&gt; <span class="title function_">queryRankList</span><span class="params">(ActivityRankTimeEnum time, <span class="type">int</span> size)</span> &#123;</span><br><span class="line">        <span class="type">String</span> <span class="variable">rankKey</span> <span class="operator">=</span> time == ActivityRankTimeEnum.DAY ? todayRankKey() : monthRankKey();</span><br><span class="line">        <span class="comment">// 1. 获取topN的活跃用户</span></span><br><span class="line">        List&lt;ImmutablePair&lt;String, Double&gt;&gt; rankList = RedisClient.zTopNScore(rankKey, size);</span><br><span class="line">        <span class="keyword">if</span> (CollectionUtils.isEmpty(rankList)) &#123;</span><br><span class="line">            <span class="keyword">return</span> Collections.emptyList();</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        <span class="comment">// 2. 查询用户对应的基本信息</span></span><br><span class="line">        <span class="comment">// 构建userId -&gt; 活跃评分的map映射，用于补齐用户信息</span></span><br><span class="line">        Map&lt;Long, Integer&gt; userScoreMap = rankList.stream().collect(Collectors.toMap(s -&gt; Long.valueOf(s.getLeft()), s -&gt; s.getRight().intValue()));</span><br><span class="line">        List&lt;SimpleUserInfoDTO&gt; users = userService.batchQuerySimpleUserInfo(userScoreMap.keySet());</span><br><span class="line"></span><br><span class="line">        <span class="comment">// 3. 根据评分进行排序</span></span><br><span class="line">        List&lt;RankItemDTO&gt; rank = users.stream()</span><br><span class="line">                .map(user -&gt; <span class="keyword">new</span> <span class="title class_">RankItemDTO</span>().setUser(user).setScore(userScoreMap.getOrDefault(user.getUserId(), <span class="number">0</span>)))</span><br><span class="line">                .sorted((o1, o2) -&gt; Integer.compare(o2.getScore(), o1.getScore()))</span><br><span class="line">                .collect(Collectors.toList());</span><br><span class="line"></span><br><span class="line">        <span class="comment">// 4. 补齐每个用户的排名</span></span><br><span class="line">        IntStream.range(<span class="number">0</span>, rank.size()).forEach(i -&gt; rank.get(i).setRank(i + <span class="number">1</span>));</span><br><span class="line">        <span class="keyword">return</span> rank;</span><br><span class="line">    &#125;</span><br></pre></td></tr></table></figure>

<p>其中核心方法是Redis的<code>zRangeWithScores</code>，用以获取指定排名的用户和对应分数。</p>
<p><code>zrevrange</code> 是Redis中的一个有序集合操作命令，用于按照分数从大到小的顺序获取有序集合中指定范围内的成员。</p>
<p>命令语法如下：<br>ZREVRANGE key start stop [WITHSCORES]<br>参数说明：</p>
<ul>
<li><code>key</code> ：有序集合的键名。</li>
<li><code>start</code> ：指定范围的起始位置，从0开始计数，表示成员的排名。</li>
<li><code>stop</code> ：指定范围的结束位置，从0开始计数，表示成员的排名。</li>
<li><code>WITHSCORES</code> （可选）：如果提供了该参数，命令会返回成员和对应的分数，以一个成员和一个分数交替排列的方式返回结果。</li>
</ul>
<blockquote>
<p>示例：<br>假设有一个有序集合名为 <code>myset</code> ，包含以下成员和对应的分数：<br>“member1” -&gt; 10<br>“member2” -&gt; 20<br>“member3” -&gt; 30<br>“member4” -&gt; 40<br>“member5” -&gt; 50<br>使用 <code>ZREVRANGE myset 0 2</code> 命令，将返回范围为0到2的成员：</p>
<ol>
<li>“member5”</li>
<li>“member4”</li>
<li>“member3”<br>使用 <code>ZREVRANGE myset 0 2 WITHSCORES</code> 命令，将返回范围为0到2的成员和对应的分数：</li>
<li>“member5”</li>
<li>“50”</li>
<li>“member4”</li>
<li>“40”</li>
<li>“member3”</li>
<li>“30”</li>
</ol>
</blockquote>
<figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">static</span> List&lt;ImmutablePair&lt;String, Double&gt;&gt; <span class="title function_">zTopNScore</span><span class="params">(String key, <span class="type">int</span> n)</span> &#123;</span><br><span class="line">        <span class="keyword">return</span> template.execute(<span class="keyword">new</span> <span class="title class_">RedisCallback</span>&lt;List&lt;ImmutablePair&lt;String, Double&gt;&gt;&gt;() &#123;</span><br><span class="line">            <span class="meta">@Override</span></span><br><span class="line">            <span class="keyword">public</span> List&lt;ImmutablePair&lt;String, Double&gt;&gt; <span class="title function_">doInRedis</span><span class="params">(RedisConnection connection)</span> <span class="keyword">throws</span> DataAccessException &#123;</span><br><span class="line">                Set&lt;RedisZSetCommands.Tuple&gt; set = connection.zRangeWithScores(keyBytes(key), -n, -<span class="number">1</span>);</span><br><span class="line">                <span class="keyword">if</span> (set == <span class="literal">null</span>) &#123;</span><br><span class="line">                    <span class="keyword">return</span> Collections.emptyList();</span><br><span class="line">                &#125;</span><br><span class="line">                <span class="keyword">return</span> set.stream()</span><br><span class="line">                        .map(tuple -&gt; ImmutablePair.of(toObj(tuple.getValue(), String.class), tuple.getScore()))</span><br><span class="line">                        .sorted((o1, o2) -&gt; Double.compare(o2.getRight(), o1.getRight())).collect(Collectors.toList());</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;);</span><br><span class="line">    &#125;</span><br></pre></td></tr></table></figure>




</article>

<div class="related-wrap reveal" id="read-next"><section class="body"><div class="item" id="prev"><div class="note">较新文章</div><a href="/2023/10/05/Spring-Cloud-Gateway%E8%AF%A6%E8%A7%A3/">Spring Cloud Gateway详解</a></div><div class="item" id="next"><div class="note">较早文章</div><a href="/2023/10/05/Redis%E6%A0%B8%E5%BF%83%E6%8A%80%E6%9C%AF%E4%B8%8E%E5%8E%9F%E7%90%86/">Redis核心技术与原理</a></div></section></div>






  <div class='related-wrap md-text reveal' id="comments">
    <section class='header cmt-title cap theme'>
      快来参与讨论吧
    </section>
    <section class='body cmt-body giscus'>
      

<svg class="loading" style="vertical-align: middle;fill: currentColor;overflow: hidden;" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2709"><path d="M832 512c0-176-144-320-320-320V128c211.2 0 384 172.8 384 384h-64zM192 512c0 176 144 320 320 320v64C300.8 896 128 723.2 128 512h64z" p-id="2710"></path></svg>

<div id="giscus" data-repo="echoalways/giscus" data-repo-id="R_kgDOKjJRiQ" data-category="Announcements" data-category-id="DIC_kwDOKjJRic4CaUxu" data-mapping="pathname" data-strict="0" data-reactions-enabled="1" data-emit-metadata="0" data-input-position="top" data-theme="preferred_color_scheme" data-lang="zh-CN" data-loading="lazy" crossorigin="anonymous"></div>

    </section>
  </div>



      
<footer class="page-footer reveal fs12"><hr><div class="text" style="text-align:center;"><p>云无心以出岫@远岫♥</p>
<div><span id="timeDate">载入天数...</span><span id="times">载入时分秒...</span>，<span id="busuanzi_container_site_pv">总访问量: <span id="busuanzi_value_site_pv"></span>次</span>，<span id="busuanzi_container_site_uv">访客数: <span id="busuanzi_value_site_uv"></span>人</span></div></div></footer>

<script async src="//busuanzi.ibruce.info/busuanzi/2.3/busuanzi.pure.mini.js"></script>
<script>
  function createtime() {
    var now = new Date();
    var grt= new Date("10/03/2023 18:40:00");
    now.setTime(now.getTime()+250);
    days = (now - grt ) / 1000 / 60 / 60 / 24; dnum = Math.floor(days);
    hours = (now - grt ) / 1000 / 60 / 60 - (24 * dnum); hnum = Math.floor(hours);
    if(String(hnum).length ==1 ){hnum = "0" + hnum;} minutes = (now - grt ) / 1000 /60 - (24 * 60 * dnum) - (60 * hnum);
    mnum = Math.floor(minutes); if(String(mnum).length ==1 ){mnum = "0" + mnum;}
    seconds = (now - grt ) / 1000 - (24 * 60 * 60 * dnum) - (60 * 60 * hnum) - (60 * mnum);
    snum = Math.round(seconds); if(String(snum).length ==1 ){snum = "0" + snum;}
    document.getElementById("timeDate").innerHTML = "本站已运行 "+dnum+" 天 ";
    document.getElementById("times").innerHTML = hnum + " 小时 " + mnum + " 分 " + snum + " 秒";
  };
  setInterval("createtime()",250);
</script>

      <div class='float-panel mobile-only blur' style='display:none'>
  <button type='button' class='sidebar-toggle mobile' onclick='sidebar.toggle()'>
    <svg class="icon" style="width: 1em; height: 1em;vertical-align: middle;fill: currentColor;overflow: hidden;" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="15301"><path d="M566.407 808.3c26.9-0.1 49.3-20.8 51.6-47.6-1.9-27.7-23.9-49.7-51.6-51.6h-412.6c-28.2-1.4-52.6 19.5-55.5 47.6 2.3 26.8 24.6 47.5 51.6 47.6h416.5v4z m309.3-249.9c26.9-0.1 49.3-20.8 51.6-47.6-2.2-26.8-24.6-47.5-51.6-47.6h-721.9c-27.7-2.8-52.5 17.4-55.3 45.1-0.1 0.8-0.1 1.7-0.2 2.5 0.9 27.2 23.6 48.5 50.7 47.6H875.707z m-103.1-245.9c26.9-0.1 49.3-20.8 51.6-47.6-0.4-28.3-23.2-51.1-51.5-51.6h-618.9c-29.5-1.1-54.3 21.9-55.5 51.4v0.2c1.4 27.8 25.2 49.2 53 47.8 0.8 0 1.7-0.1 2.5-0.2h618.8z" p-id="15302"></path><path d="M566.407 808.3c26.9-0.1 49.3-20.8 51.6-47.6-1.9-27.7-23.9-49.7-51.6-51.6h-412.6c-28.2-1.4-52.6 19.5-55.5 47.6 1.9 27.7 23.9 49.7 51.6 51.6h416.5z m309.3-249.9c26.9-0.1 49.3-20.8 51.6-47.6-2.2-26.8-24.6-47.5-51.6-47.6h-721.9c-27.7-2.8-52.5 17.4-55.3 45.1-0.1 0.8-0.1 1.7-0.2 2.5 0.9 27.2 23.6 48.5 50.7 47.6H875.707z m-103.1-245.9c26.9-0.1 49.3-20.8 51.6-47.6-0.4-28.3-23.2-51.1-51.5-51.6h-618.9c-29.5-1.1-54.3 21.9-55.5 51.4v0.2c1.4 27.8 25.2 49.2 53 47.8 0.8 0 1.7-0.1 2.5-0.2h618.8z" p-id="15303"></path></svg>
  </button>
</div>

    </div>
  </div>
  <div class='scripts'>
    <script type="text/javascript">
  const stellar = {
    // 懒加载 css https://github.com/filamentgroup/loadCSS
    loadCSS: (href, before, media, attributes) => {
      var doc = window.document;
      var ss = doc.createElement("link");
      var ref;
      if (before) {
        ref = before;
      } else {
        var refs = (doc.body || doc.getElementsByTagName("head")[0]).childNodes;
        ref = refs[refs.length - 1];
      }
      var sheets = doc.styleSheets;
      if (attributes) {
        for (var attributeName in attributes) {
          if (attributes.hasOwnProperty(attributeName)) {
            ss.setAttribute(attributeName, attributes[attributeName]);
          }
        }
      }
      ss.rel = "stylesheet";
      ss.href = href;
      ss.media = "only x";
      function ready(cb) {
        if (doc.body) {
          return cb();
        }
        setTimeout(function () {
          ready(cb);
        });
      }
      ready(function () {
        ref.parentNode.insertBefore(ss, before ? ref : ref.nextSibling);
      });
      var onloadcssdefined = function (cb) {
        var resolvedHref = ss.href;
        var i = sheets.length;
        while (i--) {
          if (sheets[i].href === resolvedHref) {
            return cb();
          }
        }
        setTimeout(function () {
          onloadcssdefined(cb);
        });
      };
      function loadCB() {
        if (ss.addEventListener) {
          ss.removeEventListener("load", loadCB);
        }
        ss.media = media || "all";
      }
      if (ss.addEventListener) {
        ss.addEventListener("load", loadCB);
      }
      ss.onloadcssdefined = onloadcssdefined;
      onloadcssdefined(loadCB);
      return ss;
    },

    // 从 butterfly 和 volantis 获得灵感
    loadScript: (src, opt) => new Promise((resolve, reject) => {
      var script = document.createElement('script');
      if (src.startsWith('/')){
        src = stellar.config.root + src.substring(1);
      }
      script.src = src;
      if (opt) {
        for (let key of Object.keys(opt)) {
          script[key] = opt[key]
        }
      } else {
        // 默认异步，如果需要同步，第二个参数传入 {} 即可
        script.async = true
      }
      script.onerror = reject
      script.onload = script.onreadystatechange = function() {
        const loadState = this.readyState
        if (loadState && loadState !== 'loaded' && loadState !== 'complete') return
        script.onload = script.onreadystatechange = null
        resolve()
      }
      document.head.appendChild(script)
    }),

    // https://github.com/jerryc127/hexo-theme-butterfly
    jQuery: (fn) => {
      if (typeof jQuery === 'undefined') {
        stellar.loadScript(stellar.plugins.jQuery).then(fn)
      } else {
        fn()
      }
    }
  };
  stellar.version = '1.19.0';
  stellar.github = 'https://github.com/xaoxuu/hexo-theme-stellar/tree/1.19.0';
  stellar.config = {
    date_suffix: {
      just: '刚刚',
      min: '分钟前',
      hour: '小时前',
      day: '天前',
      month: '个月前',
    },
    root : '/',
  };

  // required plugins (only load if needs)
  stellar.plugins = {
    jQuery: 'https://gcore.jsdelivr.net/npm/jquery@3.6.2/dist/jquery.min.js'
  };

  if ('local_search') {
    stellar.search = {};
    stellar.search.service = 'local_search';
    if (stellar.search.service == 'local_search') {
      let service_obj = Object.assign({}, {"field":"all","path":"/search.json","content":true,"sort":"-date"});
      stellar.search[stellar.search.service] = service_obj;
    }
  }

  // stellar js
  stellar.plugins.stellar = Object.assign({"sites":"/js/plugins/sites.js","friends":"/js/plugins/friends.js","ghinfo":"/js/plugins/ghinfo.js","timeline":"/js/plugins/timeline.js","linkcard":"/js/plugins/linkcard.js","fcircle":"/js/plugins/fcircle.js","weibo":"/js/plugins/weibo.js"});

  stellar.plugins.marked = Object.assign("https://cdn.bootcdn.net/ajax/libs/marked/4.0.18/marked.min.js");
  // optional plugins
  if ('false' == 'true') {
    stellar.plugins.lazyload = Object.assign({"enable":false,"js":"https://gcore.jsdelivr.net/npm/vanilla-lazyload@17.8.3/dist/lazyload.min.js","transition":"blur"});
  }
  if ('true' == 'true') {
    stellar.plugins.swiper = Object.assign({"enable":true,"css":"https://unpkg.com/swiper@8.4.5/swiper-bundle.min.css","js":"https://unpkg.com/swiper@8.4.5/swiper-bundle.min.js"});
  }
  if ('' == 'true') {
    stellar.plugins.scrollreveal = Object.assign({"enable":null,"js":"https://gcore.jsdelivr.net/npm/scrollreveal@4.0.9/dist/scrollreveal.min.js","distance":"8px","duration":500,"interval":100,"scale":1});
  }
  if ('true' == 'true') {
    stellar.plugins.preload = Object.assign({"enable":true,"service":"flying_pages","instant_page":"https://gcore.jsdelivr.net/gh/volantis-x/cdn-volantis@4.1.2/js/instant_page.js","flying_pages":"https://gcore.jsdelivr.net/gh/gijo-varghese/flying-pages@2.1.2/flying-pages.min.js"});
  }
  if ('true' == 'true') {
    stellar.plugins.fancybox = Object.assign({"enable":true,"js":"https://gcore.jsdelivr.net/npm/@fancyapps/ui@4.0/dist/fancybox.umd.js","css":"https://gcore.jsdelivr.net/npm/@fancyapps/ui@4.0/dist/fancybox.css","selector":".swiper-slide img"});
  }
  if ('false' == 'true') {
    stellar.plugins.heti = Object.assign({"enable":false,"css":"https://unpkg.com/heti@0.9.2/umd/heti.min.css","js":"https://unpkg.com/heti@0.9.2/umd/heti-addon.min.js"});
  }
  if ('true' == 'true') {
    stellar.plugins.copycode = Object.assign({"enable":true,"js":"/js/plugins/copycode.js","default_text":"Copy","success_text":"Copied"});
  }
</script>

<!-- required -->

  
<script src="/js/main.js" async></script>



<!-- optional -->

  <script>
  function loadJS() {
    const els = document.querySelectorAll("#comments #giscus");
    if (els.length === 0) return;
    els.forEach((el, i) => {
      try {
        el.innerHTML = '';
      } catch (error) {
        console.log(error);
      }
      var script = document.createElement('script');
      script.src = 'https://giscus.app/client.js';
      script.async = true;
      for (let key of Object.keys(el.attributes)) {
        let attr = el.attributes[key];
        if (['class', 'id'].includes(attr.name) === false) {
          script.setAttribute(attr.name, attr.value);
        }
      }
      el.appendChild(script);
    });
  }
  window.addEventListener('DOMContentLoaded', (event) => {
    loadJS();
  });
</script>




<!-- inject -->


  </div>
</body>
</html>
